<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>SWI-Prolog WASM tests</title>
</head>
<body>
<style>
.title  { font-weight: bold; font-size: 200%; font-family: reset;
	  margin-top: 1.5ex; margin-bottom: 1ex; display: block; }
.stderr { color: red; }
.stderr, .stdout, .query {
  white-space: pre-wrap;
  font-family: monospace;
  overflow-wrap: anywhere;
}
#output { margin-top: 1.5ex; }
#tests  { margin-bottom: 0.5ex; }
</style>
<h2>Embedded SWI-Prolog demo and tests</h2>
<p>
  This page loads the SWI-Prolog WASM version and interacts with the
  Prolog system to illustrate data exchange and various calling
  conventions.  Please examine comments in the source code for
  further explanation.
</p>
<div id="controls">
  <div id="tests"></div>
  <button onClick="select_all(true)">Select all</button>
  <button onClick="select_all(false)">Deselect all</button>
  <button onClick="run_selected_tests()">Run selected tests</button>
</div>
<div id="output"></div>

<!-- Load prolog.  Using the bundle provides a single file solution,
     which is nice for deployment.  Using `swipl-web.js` allows for
     generating a debug version.  Single file bundles do not support
     a .map file.
  -->
<script src="/wasm/swipl-bundle.js"></script>
<!-- <script src="/wasm/swipl-web.js"></script> -->

<script>
output   = document.getElementById('output');
test_div = document.getElementById('tests');

/*  Create Prolog.  This is still a bit ugly.  I am not sure (how) we can
    improve on this.  `swipl-web.js` defines a single global function `SWIPL`
    with represents the Emscripten generated module.  The object passed to
    this is populated with all WASM stuff.  We may define some initial
    properties to change the default behavior.  Notably:

     - `arguments` provide the Prolog command line arguments.  Notably
       `-q` may come handy to suppress the header.  Most other command
       line arguments have little meaning in the context of the browser
       version.  Do __not__ pass goals as we need to do some post
       initialization steps before returning.
     - `locateFile` is a function to find the other components
       (`swipl-web.wasm` and `swipl-web.data`) from the plain file.
     - `on_output` takes a function to handle output to `user_output`
       and `user_error`.  It received a line of input (a String) and
       either `"stderr"` or `"stdout"`.   When omitted, the output
       is sent to the browser console.

   The skeleton below is best I could find.  Based on `options`,
   `SWIPL()` returns a Promise that is executed after the other data
   files are loaded, Emscripten and Prolog are initialized.
   `Module.prolog` is a class that provides the high level interaction
   with Prolog.  In this example we load a file `test.pl` from the
   server (relatvie to this file) and, when ready, call `run()`.

*/

let Prolog;
let Module;
var options = {
//  arguments: ["-q"],
    locateFile: (file) => '/wasm/' + file,
    on_output: print
};

SWIPL(options).then((module) =>
{ Module = module;
  Prolog = Module.prolog;

  Prolog.consult("test.pl")
  .then(() => {
    test_menu();
    setup();
  });
});

var tests = [];
function test_menu() {
  for(let i=0; i<tests.length; i++) {
    const t = tests[i];
    const c = t.checked === false ? "" : "checked";
    test_div.innerHTML += `<div><input type="checkbox" value="${i}" name="test" ${c}><label>${t.name}</label></div>`;
  }
}

function show_test_title(title)
{ const hdr = document.createElement("h4");
  hdr.innerHTML = title;
  const out = document.createElement("div");
  output.appendChild(hdr);
  output.appendChild(out);

  return out;
}

function select_all(val) {
  const s = document.querySelectorAll("input[type=checkbox][name=test]");
  s.forEach(e => e.checked = val);
}

function getPromiseFromEvent(item, event) {
  return new Promise((resolve) => {
    const listener = (ev) => {
      item.removeEventListener(event, listener);
      resolve(ev);
    }
    item.addEventListener(event, listener);
  })
}

function done_test(test) {
  const event = new CustomEvent("done", {detail: test});
  output.dispatchEvent(event);
}

async function run_selected_tests() {
  output.innerHTML = "";
  const selected = Array
	.from(document.querySelectorAll(
	  "input[type=checkbox][name=test]:checked"),
	      e => parseInt(e.value));

  run_tests(0, selected);
  await getPromiseFromEvent(output, "done");
  print(output, "All done", {color: "green", nl:true});
}

async function run_tests(i, selected)
{ if ( i < selected.length )
  { const test = tests[selected[i]];
    const out = show_test_title(test.name);
    const rc = test.call.call(test, out);
    if ( rc instanceof Promise ) {
      console.log(`Test ${test.name} returned a Promise}`);
      await rc;
    }
    setTimeout(() => run_tests(i+1, selected));
  } else
    done_test("all");
}

/* Prepare the test cases
*/

function setup()
{ Prolog.set_arg_names("point", ["x", "y"]);
}

/* Get all results from p/1, showing iteration over Prolog answers
   and the format of the various result values.   Note that the
   `Prolog.with_frame(func)` makes sure Prolog term references
   are properly reclaimed.  This is needed to ensure `av` is
   reclaimed.
*/

////////////////////////////////////////////////////////////////
tests.push({ name: "p(X) with loop",
	     call: run_p
	   });
function run_p(out)
{ Prolog.with_frame(() =>
      { let av = Prolog.new_term_ref(1);
	let q = Prolog.query(0, Prolog.PL_Q_NORMAL, "p/1", av);
	let n = 0;

	while(q.next().value)
	{ println(out,
		  `${++n} `,
		  Prolog.get_chars(av, Prolog.CVT_WRITEQ), " -> ",
		  Prolog.toJSON(av));
	}

	q.close();
      });
}

////////////////////////////////////////////////////////////////
tests.push({ name: "p(X) with iterator",
	     call: run_p_with_iterator
	   });

/* Same as above, but using the JavaScript iterator (for..of) to enumerate
   the results.  We still need the frame to get rid of `av`.
*/

function run_p_with_iterator(out)
{ Prolog.with_frame(() =>
  { let av = Prolog.new_term_ref(1);
    let n = 0;

    for(const r of Prolog.query(0, Prolog.PL_Q_NORMAL, "p/1", av,
				(v) => Prolog.toJSON(v)))
    { println(out, r);
    }
  });
}

////////////////////////////////////////////////////////////////
tests.push({ name: "p(X) from string with iterator",
	     call: run_p_from_string_with_iterator
	   });

/* Pass a query from a string.  As the allocation of term references
   now happens completely in the `Prolog.query()` call, the
   `with_frame()` happens inside the interface and we no longer have
   to worry about leaks.
*/

function run_p_from_string_with_iterator(out)
{ let n = 0;

  for(const r of Prolog.query("p(X)"))
  { println(out, r.X);
  }
}

////////////////////////////////////////////////////////////////
tests.push({ name: "ground goal with iterator",
	     call: run_ground_with_iterator
	   });

function run_ground_with_iterator(out)
{ println(out, Prolog.query("true").once());
  println(out, Prolog.query("false").once());
  println(out, Prolog.query("functor(_,_,_)").once());
}


////////////////////////////////////////////////////////////////
tests.push({ name: "Round trip data",
	     call: test_data_round_trip
	   });

/* This test tests round trip of data.  We start with a JavaScript
   object, pass it to Prolog with returns it using u/3 defined as
   u(X,X,S), where S returns the Prolog string representation for X.
*/

class Point {
  constructor(x,y)
  { this.x = y;
    this.y = y;
  }
}

function test_data_round_trip(out)
{ trip(out,
       42,
       2n**200n,
       3.14,
       new Prolog.Rational(1,3),
       "Hello world!",
       new Prolog.String("A string"),
       [ "aap", "noot", "mies" ],
       new Prolog.List(["a1"], new Prolog.Var()),
       new Prolog.Compound("t", [1,2,"aap"]),
       { name: "Jan", location: "Amsterdam" },
       {},
       new Point(9,9),
       [ new Prolog.Var("A"), new Prolog.Var("A"), new Prolog.Var() ]
      );
}

function trip(out, ...args)
{ args.forEach((obj) =>
  { const rc = Prolog.query("u(In,Out,S)", {In:obj}).once();

    println(out, obj, " -> ", rc.S, " -> ", rc.Out);
  });
}

////////////////////////////////////////////////////////////////
tests.push({ name: "Sum array of integers in Prolog",
	     call: test_sum_list
	   });

/* Test calling a simple deterministic predicate.  Evaluates the performance
   for exchanging data.  test_error() tests what happens when the predicate
   raises an exception.  In that case the object returned by once() represents
   the error and (thus) the target Prolog variables returned as `undefined`.
*/

function sum_list(list)
{ return Prolog.query("time(sum_list(List, Sum))", {List:list}).once().Sum;
}

function test_sum_list(out)
{ sum_list([]);		// do the required autoloading
  [ 1,
    10,
    100,
    1000,
    10000,
    100000,
//  1000000
  ].forEach((len) =>
  { const list = [];
    for(var i=1; i<=len; i++)
      list.push(i);

    const sum = time("sum_list()", () => sum_list(list));
    println(out, `Sum of 1..${len} is ${sum}`);
  });
}

////////////////////////////////////////////////////////////////
tests.push({ name: "Test big integer exchange",
	     call: test_bigint
	   });
/* Test data round trip for really big integers and some critial values.
*/

function test_bigint(out)
{ [0,31,32,62,63,64,200].forEach((i) =>
  { const rc = Prolog.query("bigint(I, Pos, Neg)", {I:i}).once();
    const pos = rc.Pos;
    const neg = rc.Neg;

    println(out, `2^${i} = ${pos}; -(2^${i}) = ${neg}`);

    const vin  = BigInt(2)**BigInt(i);
    const vout = Prolog.query("In = Out", {In:vin}).once().Out;

    if ( vin === BigInt(vout) )
      println(out, `Round trip passed`);
    else
      println(out, `Round trip: ${vin} -> ${vout}`);
  });
}

////////////////////////////////////////////////////////////////
tests.push({ name: "Test simple error handling",
	     call: test_error
	   });

function test_error(out)
{ const sum = sum_list(["aap", "noot", "mies"]);
  println(out, `Sum is ${sum}`);
}

////////////////////////////////////////////////////////////////
tests.push({ name: "Test long running queries with sleep",
	     call: test_foreach_1
	   });

function test_foreach_1(out)
{ return Prolog.forEach("between(1,3,X),sleep(0.5)",
			(r) => println(out, r.X))
           .then((n) => println(out, `Got ${n} answers`))
           .catch((e) => println(out, `Got error ${e}`));
}

////////////////////////////////////////////////////////////////
tests.push({ name: "Test long running queries with sleep and error",
	     call: (out) => test_foreach_2(out)
	   });

function test_foreach_2(out, mx)
{ mx = mx||5;

  println("title", "Run long running goal; show exception");
  return Prolog.forEach("between(1,MAX,X),sleep(0.5),Y is 1/(3-X)",
			{MAX:mx},
			(r) => println(out, r.X))
           .then((n) => println(out, `Got ${n} answers`))
           .catch((e) => println(out, `Got error ${e}`));
}

tests.push({ name: "Test long running computation",
	     call: (out) => test_fib(out, 35)
	   });
tests.push({ name: "Test long running computation (configurable)",
	     call: test_fib,
	     checked: false
	   });

function test_fib(out, n) {
  if ( n === undefined ) {
    out.innerHTML += `<form onsubmit="return fib_n(this)">
                        <input name="n" type="number" min="1" max="40" value="22">
			<button type="submit">Run</button>
                      </form>`;
  } else {
    println(out, `While computing the <textarea> must be responsive
and it is possible to abort the computation`);

    out.innerHTML += `
<div>
<textarea cols="50" placeholder="Type here to see interaction"></textarea>
</div>
<div>
<button id="abort" onclick="Prolog.abort()">Abort</button>
</div>`;

    return Prolog.forEach("time(fib(X,Y))", {X:n})
      .then((a) => println(out, `fib(${n}) => ${a[0].Y}`))
      .catch((e) => print(out, `Got error ${e}`, {color:"red", nl:true}))
      .finally(() => document.getElementById("abort").disabled = true);
  }
}

function fib_n(e) {
  const n = e.elements.n.value;
  test_fib(parseInt(n));
  return false;
}

/* Callback test */

function add_one(i)
{ return i+1;
}


		 /*******************************
		 *           PRINTING           *
		 *******************************/

function isElement(element)
{ return ( element instanceof Element ||
           element instanceof HTMLDocument );
}

/** print([to], line, [opts])
 */
function print(...argv)
{ const node = document.createElement('span');
  let out = output;

  if ( isElement(argv[0]) )
  { out = argv[0];
    argv.shift();
  }
  let line = argv[0];
  opts = argv[1];
  opts = opts||{};

  const cls = opts.cls||"stdout";
  if ( typeof(line) === "object" )
    line = JSON.stringify(line);
  if ( opts.nl )
    line += '\n';
  if ( opts.background )
    node.style['background'] = opts.background;
  if ( opts.color )
    node.style['color'] = opts.color;

  node.className = cls;
  node.textContent = line;
  out.appendChild(node);
};

function println(to, ...line)
{ let out = output;
  if ( isElement(to) )
  { out = to;
    line.forEach((e) => print(to, e, {cls:"stdout"}));
  } else
  { line.forEach((e) => print(e, {cls:to}));
  }
  out.appendChild(document.createElement('br'));
}

function time(msg, func)
{ const t0 = Date.now();
  const rc = func.call(window);
  const t1 = Date.now();

  println("stdout", `${msg} took ${(t1-t0)}ms`);

  return rc;
}

</script>
</body>
</html>
